using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Threading;
using TrackMe.Apps.VM;
using TrackMe.Backend.Entities;
using TrackMe.Dto;

[Export(typeof(IDatabase))]
public class Dao : IDatabase
{
    private DateTime lastSaved;
	private readonly TimeSpan beatSaveInterval = TimeSpan.FromMinutes(5);

	public Dao()
	{
		Startup();
	}

	private static IJob MakeJob(int jobId)
	{
		var result = AutoIntf.New<IJob>();
		result.Id = jobId;
		return result;
	}

	public void AddUser(string name, string guid)
	{
	    using (var db = Db)
	    {
	        var user = new User {Guid = guid, Name = name};
            db.Users.Add(user);
	        db.SaveChanges();
	    }
	}

    private TimeTrackContext Db
    {
        get { return new TimeTrackContext(); }
    }


    int IDatabase.SaveProject(IEditProject prj)
	{
        using (var db = Db)
        {
		    Project project;
		    if (prj.Id == 0)
		    {
		        project = new Project();
                db.Projects.Add(project);
            }
		    else 
                project = db.Projects.Find(prj.Id);
		    project.Name = prj.Name;
            db.SaveChanges();
            return project.Id;
        }
	}

    public int SaveTask(IEditTask model)
    {
    	Task task;
        using (var db = Db)
        {
            if (model.Id == 0)
            {
                task = new Task();
                db.Tasks.Add(task);
            }
            else task = db.Tasks.Find(model.Id);
            task.Name = model.Name;
            task.ProjectId = model.ProjectId;
            db.SaveChanges();
		}
        return task.Id;
	}

    public void StopTask(int jobId)
	{
        using (var db = Db)
        {
			var job = db.Jobs.Find(jobId);
			job.EndTime = DateTime.Now;
			var jobInProgress = db.JobsInProgress.FirstOrDefault();
			if (jobInProgress != null)
                db.JobsInProgress.Remove(jobInProgress);
            db.JobsToClockOut.Add(new JobToClockOut{Job = job});
            db.SaveChanges();
		}
	}

	public IJob StartTask(int taskId, string description)
	{
        using (var db = Db)
        {
			Task task = db.Tasks.Find(taskId);
			var newJob = new Job
				{
					Task = task,
					StartTime = DateTime.Now,
					EndTime = DateTime.Now,
					Description = description
				};
            db.Jobs.Add(newJob);
			var jobInProgress = new JobInProgress { Job = newJob };
			db.JobsInProgress.Add(jobInProgress);
            db.SaveChanges();
            return MakeJob(newJob.Id);
		}
	}

	public IList<ITaskForSelect> RecentTasks(int atMost)
	{
        using (var db = Db)
        {
		    var query = (from job in db.Jobs
					    orderby job.EndTime descending
				        select new
				        {
				            TaskId = job.Task.Id, 
                            TaskName = job.Task.Name, 
                            job.Task.ExternalId, 
                            Project = job.Task.Project.Name, 
                            Parent = job.Task.Parent.Name
				        })
				.Take(atMost)
                .Distinct()
				.ToList();

			var result = new List<ITaskForSelect>();
			foreach (var job in query)
			{
				var item = AutoIntf.New<ITaskForSelect>();
				item.TaskId = job.TaskId;
				item.TaskName = TaskCaption(job.TaskName, job.ExternalId);
				item.ExternalId = job.ExternalId;
				item.ProjectName = string.IsNullOrEmpty(job.Parent) ? job.Project : job.Parent;
				result.Add(item);
			}
			return result;
		}
	}

	private static string TaskCaption(string taskName, int externalId)
	{
		if (externalId > 0)
			return string.Format("{0}: {1}", externalId, taskName);
		return taskName;
	}

	public IList<ITaskForSelect> SearchForTask(string searchFor)
	{
        using (var db = Db)
		{
			int id;
			Int32.TryParse(searchFor, out id);
			var tasks =
				from task in db.Tasks
				where task.Name.Contains(searchFor) || (id >  0 && task.ExternalId == id)
				select new { TaskId = task.Id, TaskName = task.Name, Project = task.Project.Name, task.ExternalId, Parent = task.Parent.Name };
			var result = new List<ITaskForSelect>();
			foreach (var task in tasks)
			{
				var item = AutoIntf.New<ITaskForSelect>();
				item.TaskId = task.TaskId;
				item.ExternalId = task.ExternalId;
				item.TaskName = TaskCaption(task.TaskName, task.ExternalId);
				item.ProjectName = string.IsNullOrEmpty(task.Parent) ? task.Project: task.Parent;
				result.Add(item);
			}
			return result;
		}
	}

	public IList<IProjectForList> ProjectList()
	{
        using (var db = Db)
		{
			var result = new List<IProjectForList>();
		    foreach (var prj in db.Projects)
			{
				var item = AutoIntf.New<IProjectForList>();
				item.Id = prj.Id;
				item.Name = prj.Name;
				result.Add(item);
			}
			return result;
		}
	}

    public IList<ITaskForList> ProjectTask(int projectId)
    {
        using (var db = Db)
		{
			var result = new List<ITaskForList>();
		    foreach (var task in db.Tasks.Where(task => task.ProjectId == projectId))
			{
				var item = AutoIntf.New<ITaskForList>();
				item.Id = task.Id;
				item.Name = task.Name;
				result.Add(item);
			}
			return result;
		}
    }

	public static int GetWeekNumber(DateTime date)
	{
		CultureInfo culture = CultureInfo.CurrentCulture;
		int weekNum = culture.Calendar.GetWeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
		return weekNum;
	}

	DateTime StartOfWeek(DateTime date)
	{
		CultureInfo info = Thread.CurrentThread.CurrentCulture;
		DayOfWeek firstday = info.DateTimeFormat.FirstDayOfWeek;
		DayOfWeek today = info.Calendar.GetDayOfWeek(date);
		int diff = today - firstday;
		return date.AddDays(-diff);
	}

	DateTime StartOfSpecificWeek(int weekNum)
	{
		var startOfYear = new DateTime(DateTime.Now.Year, 1, 1);
		return StartOfWeek(startOfYear).AddDays(weekNum * 7);
	}

    public IReport Report(ReportPeriod period, ReportKind kind)
    {
        var result = AutoIntf.New<IReport>();
        var rows = new List<object[]>();
        var columns = new List<string>();


        using (var db = Db)
		{
			DateTime startDate = DateTime.Now;
			DateTime endDate = DateTime.Now;
			DateTime weekStart = StartOfWeek(DateTime.Now);
			var monthStart = new DateTime(startDate.Year, startDate.Month, 1);
			switch (period)
			{
				case ReportPeriod.Today:
					startDate = DateTime.Today;
					break;
				case ReportPeriod.ThisWeek:
					startDate = weekStart;
					break;
				case ReportPeriod.LastWeek:
					startDate = weekStart.AddDays(-7);
					endDate = weekStart.AddMilliseconds(-1);
					break;
				case ReportPeriod.ThisMonth:
					startDate = monthStart;
					break;

				case ReportPeriod.PriorMonth:
					startDate = monthStart.AddMonths(-1);
					endDate = monthStart.AddMilliseconds(-1);
					break;

			}

			var chunks = (
				from job in db.Jobs
				where job.EndTime > startDate && job.StartTime < endDate
				orderby job.EndTime
				select new
				{
					ProjectName = job.Task.Project.Name,
					ParentName = job.Task.Parent.Name,
					job.Task.ExternalId,
					TaskName = job.Task.Name,
					job.StartTime,
					job.EndTime
				}).ToArray();

			// adjust boundaries
			chunks = (
				from c in chunks
				select new
				{
					c.ProjectName,
					c.ParentName,
					c.ExternalId,
					c.TaskName,
					StartTime = new[]{c.StartTime, startDate}.Max(),
					EndTime = new[]{c.EndTime, endDate}.Min()
				}).ToArray();


			
			IQueryable query;
			switch (kind)
			{
				case ReportKind.TimeSheet:
					query = (
						from c in chunks
						select c).AsQueryable();
					break;

				case ReportKind.CapitalizedSalaries:
					query = (
						from c in chunks
						group c by new {weekNum = GetWeekNumber(c.StartTime), c.TaskName} into g
						select new
						{
							WkStart = StartOfSpecificWeek(g.Key.weekNum), 
							WkEnd = StartOfSpecificWeek(g.Key.weekNum).AddDays(6), 
							Hours = Math.Round(g.Sum(s => (s.EndTime - s.StartTime).TotalHours), 2),
							Task = g.Key.TaskName
						}
					).AsQueryable();
					break;
                default: 
                    throw new Exception("Unexpected " + kind);
			}

			var type = query.ElementType;
			foreach (var col in type.GetProperties())
				columns.Add(col.Name);
			foreach (var rec in query)
		    {
		        var row = new List<object>();
		    	foreach (var prop in rec.GetType().GetProperties())
		    		row.Add(prop.GetValue(rec, null) ?? string.Empty);
		    	rows.Add(row.ToArray());
		    }
		}
        result.Cells = rows.ToArray();
        result.Columns = columns.ToArray();
        return result;
    }

	public void Heartbeat(int jobId)
	{
		if (jobId == 0)
			return;

		if (lastSaved > DateTime.Now)
			lastSaved = DateTime.Now; // DST?

		if (DateTime.Now - lastSaved < beatSaveInterval)
			return;

        using (var db = Db)
        {
		    var job = db.Jobs.Find(jobId);
		    job.EndTime = DateTime.Now;
            db.SaveChanges();
		}

	    lastSaved = DateTime.Now;
	}

	public IJob JobInProgress()
	{
        using (var db = Db)
		{
			var jobInProgress = db.JobsInProgress.FirstOrDefault();
			return jobInProgress != null ? MakeJob(jobInProgress.JobId) : null;
		}
	}

	public IJob ResumeOrpahJob(int jobId)
	{
        using (var db = Db)
        {
		    var job = db.Jobs.Find(jobId);
		    job.EndTime = DateTime.Now;
            db.SaveChanges();
		}
	    return MakeJob(jobId);
	}

	public void CancelOrphanJob(int jobId)
	{
        using (var db = Db)
        {
			var jobInProgress = db.JobsInProgress.First();
			Debug.Assert(jobInProgress.JobId == jobId);
            db.JobsInProgress.Remove(jobInProgress);
            db.SaveChanges();
		}
	}

	public IList<ITfsServer> TfsServerList()
	{
        using (var db = Db)
		{
			var result = new List<ITfsServer>();
		    foreach (var srv in db.TfsServers)
			{
				var server = AutoIntf.New<ITfsServer>();
				server.Id = srv.Id;
				server.LastSyncTime = srv.LastSync;
				server.Url = srv.Url;
				server.Name = srv.Name;
				result.Add(server);
			}
			return result;
		}
	}

	public int SaveTfsServer(ITfsServer server)
	{
        using (var db = Db)
        {
			TfsServer srv;
            if (server.Id == 0)
            {
                srv = new TfsServer();
                db.TfsServers.Add(srv);

            }
            else srv = db.TfsServers.Find(server.Id);
            srv.Url = server.Url;
            srv.LastSync = DateTime.Today.AddYears(-10);
            db.SaveChanges();
			return srv.Id;
		}
	}

	void IDatabase.ImportTfsItems(int serverId, DateTime lastSyncTime, IList<ITfsWorkItem> workItems)
	{
        using (var db = Db)
        {
			TfsServer srv = db.TfsServers.Find(serverId);
			srv.LastSync = lastSyncTime;
			var postpone = workItems.Count;
			postpone = postpone * (postpone + 1) / 2;

			while(workItems.Count > 0)
			{
				var item = workItems[0];
				workItems.RemoveAt(0);

				Task parent = null;
				if (item.ParentId > 0)
				{
					parent = db.Tasks.FirstOrDefault(t => t.ExternalId == item.ParentId);
					if (parent == null)
					{
						if (--postpone == 0)
							throw new Exception("Cyclic dependency of tasks"); 
						workItems.Add(item);
						continue;
					}
				}

				var project = db.Projects.FirstOrDefault(p => p.ExternalId == item.ProjectId);
				if (project == null)
				{
					project = new Project
					{
						ExternalId = item.ProjectId, 
						Name = item.ProjectName,
                        ServerId = serverId
					};
					db.Projects.Add(project);
				    db.SaveChanges();
				}

				var task = db.Tasks.FirstOrDefault(t => t.ExternalId == item.Id);
                if (task == null)
                {
                    task = new Task();
                    db.Tasks.Add(task);
                }
				task.ExternalId = item.Id;
				task.Name = item.Title;
				task.Project = project;
				task.Parent = parent;
                db.SaveChanges();
            }
		}
	}

    int[] IDatabase.FindAbsentTasks(int[] ids)
	{
        using (var db = Db)
        {
		    int[] exist = db.Tasks.Where(t => ids.Contains(t.ExternalId)).Select(t => t.Id).ToArray();
			return ids.Except(exist).ToArray();
		}
	}

    IClockOut[] IDatabase.PendingClockOuts()
    {
        using (var db = Db)
        {
            var list = new List<IClockOut>();
            foreach (var clockout in db.JobsToClockOut.ToArray())
            {
                if (clockout == null)
                    return null;
                var job = clockout.Job;

                var result = AutoIntf.New<IClockOut>();
                result.Time = job.EndTime - job.StartTime;
                result.ExternalId = job.Task.ExternalId;
                result.JobId = job.Id;
                result.ServerId = job.Task.Project.ServerId;
                list.Add(result);
            }
            return list.ToArray();
        }
    }

	void IDatabase.ClockOutDone(int[] jobIds)
    {
        using (var db = Db)
        {
	        foreach (var clockout in db.JobsToClockOut.Where(c => jobIds.Contains(c.JobId)).ToList())
		        db.JobsToClockOut.Remove(clockout);
	        db.SaveChanges();
        }
    }


    IUserForList IDatabase.Login(string name, string guid)
	{
        using (var db = Db)
        {
			var user = db.Users.FirstOrDefault(u => u.Guid == guid);
			if (user == null)
			{
				user = new User
					{
						Name = name,
						Guid = guid
					};
				db.Users.Add(user);
			}
			var result = AutoIntf.New<IUserForList>();
			result.Id = user.Id;
			result.Name = user.Name;
			return result;
		}
	}

	public void Close()
	{
	}

	public IStatMatrix LoadStats(int taskId)
	{
        using (var db = Db)
        {
			Task task = taskId == 0 ? null : db.Tasks.Find(taskId);
			Project prj = task == null ? null : task.Project;
			var result = AutoIntf.New<IStatMatrix>();

			var monthBegin = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
			result.TodayTotal = SelectTotal(db, DateTime.Today, DateTime.Today.AddDays(1), null, null);
			result.MonthTotal = SelectTotal(db, monthBegin, monthBegin.AddMonths(1), null, null);
			result.TotalTotal = SelectTotal(db, DateTime.MinValue, DateTime.MaxValue, null, null);

			result.TodayProject = SelectTotal(db, DateTime.Today, DateTime.Today.AddDays(1), prj, null);
			result.MonthProject = SelectTotal(db, monthBegin, monthBegin.AddMonths(1), prj, null);
			result.TotalProject = SelectTotal(db, DateTime.MinValue, DateTime.MaxValue, prj, null);

			result.TodayTask = SelectTotal(db, DateTime.Today, DateTime.Today.AddDays(1), null, task);
			result.MonthTask = SelectTotal(db, monthBegin, monthBegin.AddMonths(1), null, task);
			result.TotalTask = SelectTotal(db, DateTime.MinValue, DateTime.MaxValue, null, task);

			return result;
		}
	}

	private static TimeSpan SelectTotal(TimeTrackContext db, DateTime periodStart, DateTime periodEnd, Project prj, Task task)
	{
		var jobs = from job in db.Jobs
			where job.EndTime >= periodStart && job.StartTime < periodEnd
			select job;

		if (prj != null)
			jobs = jobs.Where(job => job.Task.ProjectId == prj.Id);

		if (task != null)
			jobs = jobs.Where(job => job.TaskId == task.Id);

		long totalTicks = 0;
		foreach (var job in jobs.Select(job => new { job.StartTime, job.EndTime }))
		{
			var start = Math.Max(job.StartTime.Ticks, periodStart.Ticks);
			var end = Math.Min(job.EndTime.Ticks, periodEnd.Ticks);

			totalTicks += end - start;
		}
		return TimeSpan.FromTicks(totalTicks);
	}

	private void Startup()
	{
        bool anyUsers;
	    using (var db = Db)
	        anyUsers = !db.Users.Any();

	    if (anyUsers)
	        FillModelWithDefaultData();
	}

	void FillModelWithDefaultData()
	{
        using (var db = Db)
        {
			var u = new User();
			WindowsIdentity currentWindowsUser = WindowsIdentity.GetCurrent();
		    Debug.Assert(currentWindowsUser != null);
		    u.Name = currentWindowsUser.Name;
	        Debug.Assert(currentWindowsUser.User != null);
	        u.Guid = currentWindowsUser.User.Value;
			db.Users.Add(u);
            db.SaveChanges();
		}
	}
}
